/*
 *     This file is part of Mobile GPS Logger.
 *
 *     Mobile GPS Logger is free software: you can redistribute it and/or
 *     modify it under the terms of the GNU General Public License as
 *     published by the Free Software Foundation, either version 3 of the
 *     License, or (at your option) any later version.
 *
 *     Mobile GPS Logger is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License v3 for more details.
 *
 *     You should have received a copy of the GNU General Public License v3
 *     along with Mobile GPS Logger. If not, see <http://www.gnu.org/licenses/>.
 */

package cx.ath.skyflyer.gpslog;

import cx.ath.skyflyer.gpslog.gps.Position;
import cx.ath.skyflyer.gpslog.io.LineReader;
import cx.ath.skyflyer.gpslog.nmea.*;
import cx.ath.skyflyer.gpslog.util.M;
import cx.ath.skyflyer.gpslog.util.StringUtils;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.Calendar;
import java.util.Enumeration;
import java.util.NoSuchElementException;
import java.util.TimeZone;
import java.util.Vector;
import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;
import javax.microedition.io.file.FileConnection;
import javax.microedition.lcdui.*;

// TODO: Add some indicators:
//  * number of satellites?
//  * distance travelled
//  * 

/**
 *
 * @author Ahti Legonkov
 */
public class Logger implements Runnable {

    Logger(GpsLogMidlet midlet, Displayable prev, String sourceUrl) {

        m_SourceUrl = sourceUrl;
        m_Midlet = midlet;
        m_Prev = prev;
        Display display = Display.getDisplay(m_Midlet);

        m_Form = new LoggerForm(this);
        display.setCurrent(m_Form);

        m_Alert = new cx.ath.skyflyer.gpslog.ui.Alert(m_Midlet);
    }

    //
    // Implementation of Runnable
    //

    private static String getOutputStreamFileName() {
        final String dir = Configuration.getLogPath();
        final Calendar now = Calendar.getInstance(TimeZone.getDefault());
        final String gpsDataPath = dir + calendarToString(now) + ".txt";

        return gpsDataPath;
    }

    private static OutputStream openOutputStream(String filename) throws IOException {
        
        final FileConnection fileConn = (FileConnection)Connector.open(filename, Connector.READ_WRITE);
        if (!fileConn.exists()) {
            fileConn.create();
        }

        final OutputStream out = fileConn.openOutputStream();
        return out;
    }

    private Vector m_NmeaListeners = new Vector();

    private void addSentenceListener(NmeaSentenceListener nsl) {
        if (nsl != null && !m_NmeaListeners.contains(nsl))
            m_NmeaListeners.addElement(nsl);
    }

    private void removeSentenceListener(NmeaSentenceListener nsl) {
        if (nsl != null && m_NmeaListeners.contains(nsl))
            m_NmeaListeners.removeElement(nsl);
    }

    /**
     * 
     * @param nsl
     * @return true if this.hasSentenceListener nsl and false otherwise.
     */
    private boolean hasSentenceListener(NmeaSentenceListener nsl) {
        return nsl != null && m_NmeaListeners.contains(nsl);
    }

    /**
     *
     * @param line an NMEA 0183 sentence.
     */
    private void processSentence(String line) {

        Enumeration en = m_NmeaListeners.elements();
        while (en.hasMoreElements()) {
            NmeaSentenceListener nsl = (NmeaSentenceListener)en.nextElement();
            nsl.sentenceReceived(line);
        }
    }

    private class UpdateUiNmeaListener implements NmeaSentenceListener {

        private GpsFixData m_PrevGpsFixData = null;

        public void sentenceReceived(String line) {
            // Global Positioning System Fix Data
            if (line.startsWith(GpsFixData.PREFIX)) {
                try {
                    GpsFixData gpsFixData = new GpsFixData(line);
                    m_Form.setGpsAltitude(gpsFixData.getAltitude(), gpsFixData.getAltitudeUnit().toLowerCase());
                    m_Form.setGpsTime(gpsFixData.getTime());
                    m_Form.setGpsFixQuality(gpsFixData.getFixQuality());
                    m_Form.setGpsFixQuality(gpsFixData.getFixQuality());

                    if (m_PrevGpsFixData != null) {
                        double deltaAltitude = gpsFixData.getAltitude() - m_PrevGpsFixData.getAltitude();
                        double deltaTime = gpsFixData.getTime().sub(m_PrevGpsFixData.getTime());
                        double speed = deltaAltitude / deltaTime;
                        m_Form.setVerticalSpeed(speed, gpsFixData.getAltitudeUnit().toLowerCase()+"/s");
                    }

                    m_PrevGpsFixData = gpsFixData;
                }
                catch (NumberFormatException ex) {
                }
                catch (ChecksumMismatchException ex) {
                }
                catch (NoSuchElementException ex) {
                    m_Form.setGpsAltitude(Double.NaN, "");
                    m_Form.setGpsFixQuality(0);
                    m_Form.setGpsTime(null);
                }
                catch (UnsupportedTypeException ex) {
                    m_Form.setGpsAltitude(Double.NaN, "");
                    m_Form.setGpsFixQuality(0);
                    m_Form.setGpsTime(null);
                }
                catch (Exception ex) {
                    m_Form.setGpsTime(null);
                }
            }
            else if (line.startsWith(GroundSpeed.PREFIX)) {
                try {
                    GroundSpeed gs = new GroundSpeed(line);
                    m_Form.setGroundSpeed(gs.getGroundSpeedKmh(), "km/h", gs.getTrueCourse());
                }
                catch (Exception ex) {
                    m_Form.setGroundSpeed(0, "Error:"+ex, "");
                }
            }
        }
    }

    /**
     * Writes raw stream to a file.
     */
    private class FileWriteNmeaListener implements NmeaSentenceListener {

        private OutputStreamWriter out;
        public final String fileName;

        public FileWriteNmeaListener() throws IOException {
            fileName = Logger.getOutputStreamFileName();
            out = new OutputStreamWriter(Logger.openOutputStream(fileName));
        }

        public void sentenceReceived(String nmea) {
            try {
                // assert(out != null);
                out.write(nmea);
                out.write("\r\n");
            }
            catch (IOException ex) {
                // TODO: do something sensible with the exception
            }
        }

        public void close() {
            try {
                out.flush();
                out.close();
            }
            catch (IOException ex) {
                // TODO: do something sensible with the exception.
            }
            finally {
                out = null;
            }
        }
    }

    /**
     * When this listener receives a GpsFixData sentence, it will convert this
     * sentence into a bit friendlier format for the chart component.
     */
    private class ConvertingNmeaListener implements NmeaSentenceListener {

        private OutputStreamWriter out = null;

        public ConvertingNmeaListener() {
            try {
                String fileName = Logger.getOutputStreamFileName()+"2";
                out = new OutputStreamWriter(Logger.openOutputStream(fileName));
            }
            catch (IOException ex) {
                out = null;
            }
        }

        public void sentenceReceived(String nmea) {
            if (out == null)
                return;
            
            try {
                GpsFixData fix = new GpsFixData(nmea);

                // nothing to do with a no-fix sentence.
                if (fix.getFixQuality() <= 0)
                    return;

                Position pos = new Position(fix);

                StringBuffer sb = new StringBuffer(80);
                sb
                        .append(fix.getTime().toString()).append(':')
                        .append(fix.getLatitude()).append(':')
                        .append(fix.getLongitude()).append(':');
                if (prevPos == null)
                    sb.append("0:");
                else
                    sb.append(pos.distanceFrom(prevPos)).append(':');

                if (prevFix == null)
                    sb.append("0");
                else
                    sb.append(prevFix.getAltitude() - fix.getAltitude());

                // assert(out != null);
                out.write(nmea);
                out.write("\r\n");

                prevPos = pos;
                prevFix = fix;
            }
            catch (Exception ex) {
            }
        }

        public void close() {
            try {
                out.flush();
                out.close();
                out = null;
            }
            catch (Exception ex) {
            }
        }

        private void wgsToLEest(double phi, double lambda) {
            final double lambda0 = 0.41887902047863912;
            final double a = 6378137.0000;
            final double n = 0.85417585808707841;
            final double F = 1.7988478514014741;
            final double rho0 = 4020205.4786358215;
            final double N0 = 6375000.0;
            final double E0 = 500000.0;
            final double e = 0.081819191042815792;

            final double z = e*Math.sin(phi);
            final double t = Math.tan(Math.PI/4 - phi/2) / M.pow( (1 - z) / (1 + z), e/2 );
            final double gamma = n * (lambda - lambda0);
            final double rho = a * F * M.pow(t, n);
            N = N0 + rho0 - rho*Math.cos(gamma);
            E = E0 + rho*Math.sin(gamma);
        }

        private double N = 0.0;
        private double E = 0.0;

        private Position prevPos = null;
        private GpsFixData prevFix = null;
    }

    private void showConnectingInProgress() {
        Form f = new Form("Connecting...");
        f.append(new Gauge("", false, Gauge.INDEFINITE, Gauge.CONTINUOUS_RUNNING));
        //f.addCommand(CMD_STOP);
        //f.setCommandListener(m_CommandListener);
        Display display = Display.getDisplay(m_Midlet);
        display.setCurrent(f);
    }
    /**
     * In order to not block the UI, the logging is performed in a background
     * thread. The thread is stopped when m_IsRunning is set to false.
     */
    public void run() {
        Display display = Display.getDisplay(m_Midlet);
        while (m_IsRunning) {
            try {
                showConnectingInProgress();

                StreamConnection gpsConn = (StreamConnection)Connector.open(m_SourceUrl);
                InputStreamReader gpsStream = new InputStreamReader(gpsConn.openInputStream());
                LineReader in = new LineReader(gpsStream);

                display.setCurrent(m_Form);

                //
                if (this.hasSentenceListener(m_FileWriter))
                    m_Form.resume();
                else
                    m_Form.pause();

                while (m_IsRunning)
                {
                    if (!gpsStream.ready()) {
                        try {
                            Thread.sleep(50);
                        }
                        catch (InterruptedException ex) {

                        }
                        continue;
                    }

                    String line = in.readString();

                    if (line.length() == 0)
                        continue;
                        
                    // a simple check to see if the line may be an NMEA sentence.
                    if (line.charAt(0) != '$')
                        continue;

                    processSentence(line);
                }

                in.close();
                gpsConn.close();
            }
            catch (IOException ex) {
                String msg;
                if (ex.getMessage() != null)
                    msg = "I/O Error:" + ex.getMessage();
                else
                    msg = "I/O Error.";

                Alert alert = new Alert(m_Form.getTitle(), msg, null, AlertType.ERROR);
                alert.setTimeout(2000);
                Display.getDisplay(m_Midlet).setCurrent(alert, m_Form);
            }
            catch (Throwable t) {
            }
            finally {
            }
        }

        // assert(m_Form != null);

        m_Form.stop();
    }

    //
    // Implementation of DataSource
    //

    public String getData() {
        return m_SourceUrl;
    }

    public void reportError(String msg, Exception e) {
        m_Alert.display("Error Writing to Tag! " + e.toString(), AlertType.ERROR);
    }

    //
    // Private parts
    //

    /**
     * Shows the last log file on a chart.
     */
    protected void onCmdChart() {
        try {
            final SpeedChart chart = new SpeedChart(m_Midlet);
            FileNameListener fnl = new FileNameListener() {

                public void fileSelected(String filePath) {
                    try {
                        // TODO: add some sort of indicaor to let the user know
                        // that somethin is happening and the application has
                        // not hung.
                        chart.loadFile(filePath);
                    }
                    catch (IOException ex) {
                        Alert a = new Alert("Error", ex.toString(), null, AlertType.ERROR);
                        m_Midlet.getDisplay().setCurrent(a, m_Form);
                    }
                }
            };

            //FileList fl = new FileList(fnl, m_Midlet, chart);
            //m_Midlet.getDisplay().setCurrent(fl);
        }
        catch (Exception ex) {
            m_Alert.display("Chart error: "+ex, AlertType.ERROR);
        }
    }

    protected void onCmdBack() {
        stopLogging();
        Display disp = Display.getDisplay(m_Midlet);
        if (m_Prev != null)
            disp.setCurrent(m_Prev);
    }

    protected void onCmdStart() {
        startLogging();
        addSentenceListener(m_UiUpdater);

        final boolean startImmediately = Configuration.getLoggingstartType();
        m_Form.start(startImmediately);
    }

    private void closeFile() {
        if (m_FileWriter == null)
            return;

        removeSentenceListener(m_FileWriter);
        m_FileWriter.close();
        m_FileWriter = null;

        //removeSentenceListener(m_Converter);
        //m_Converter.close();
        //m_Converter = null;
    }

    protected void onCmdStop() {
        stopLogging();
        removeSentenceListener(m_UiUpdater);
        closeFile();
        m_Form.stop();
    }

    protected void onCmdPause() {
        removeSentenceListener(m_FileWriter);
        m_Form.pause();
    }

    protected void onCmdResume() {
        addSentenceListener(m_FileWriter);
        m_Form.resume();
    }

    private void stopLogging() {
        try {
            m_IsRunning = false;
            if (m_LoggerThread != null) {
                m_LoggerThread.interrupt();
                m_LoggerThread.join();
            }
            m_LoggerThread = null;
        }
        catch (InterruptedException ex) {
        }
        finally {
            closeFile();
        }
    }

    private FileWriteNmeaListener m_FileWriter = null;
    private final UpdateUiNmeaListener m_UiUpdater = new UpdateUiNmeaListener();
    //private ConvertingNmeaListener m_Converter = null;

    private void startLogging() {
        try {
            final boolean startImmediately = Configuration.getLoggingstartType();
            
            m_FileWriter = new FileWriteNmeaListener();
            if (startImmediately)
                addSentenceListener(m_FileWriter);

            //m_Converter = new ConvertingNmeaListener();
            //addSentenceListener(m_Converter);

            m_LoggerThread = new Thread(this);
            m_IsRunning = true;
            m_LoggerThread.start();
        }
        catch (IOException ex) {
            m_Alert.display("failed to create file writer:" + ex.getMessage(), AlertType.ERROR);
            stopLogging();
        }
    }

    private static StringBuffer intToString(StringBuffer sb, int n, int places) {
        Integer i = new Integer(n);
        String s = i.toString();

        for (int j = 0; j < places - s.length(); ++j) {
            sb.append('0');
        }

        sb.append(s);

        return sb;
    }

    private static String calendarToString(Calendar cal) {
        StringBuffer sb = new StringBuffer();

        intToString(sb, cal.get(Calendar.YEAR), 4).append('-');
        intToString(sb, cal.get(Calendar.MONTH)+1, 2).append('-');
        intToString(sb, cal.get(Calendar.DAY_OF_MONTH), 2).append('-');
        intToString(sb, cal.get(Calendar.HOUR_OF_DAY), 2).append('-');
        intToString(sb, cal.get(Calendar.MINUTE), 2).append('-');
        intToString(sb, cal.get(Calendar.SECOND), 2);

        return sb.toString();
    }

    private cx.ath.skyflyer.gpslog.ui.Alert m_Alert;
    private final LoggerForm m_Form;
    private final String m_SourceUrl;
    private final GpsLogMidlet m_Midlet;
    private final Displayable m_Prev;

    private Thread m_LoggerThread;
    private volatile boolean m_IsRunning = false;
}
